# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::BackgroundMigration::FixVulnerabilityOccurrencesWithHashesAsRawMetadata, schema: 20211209203821 do
  let(:users) { table(:users) }
  let(:namespaces) { table(:namespaces) }
  let(:projects) { table(:projects) }
  let(:scanners) { table(:vulnerability_scanners) }
  let(:identifiers) { table(:vulnerability_identifiers) }
  let(:findings) { table(:vulnerability_occurrences) }

  let(:user) { users.create!(name: 'Test User', projects_limit: 10, username: 'test-user', email: '1') }

  let(:namespace) do
    namespaces.create!(
      owner_id: user.id,
      name: user.name,
      path: user.username
    )
  end

  let(:project) do
    projects.create!(namespace_id: namespace.id, name: 'Test Project')
  end

  let(:scanner) do
    scanners.create!(
      project_id: project.id,
      external_id: 'test-scanner',
      name: 'Test Scanner',
      vendor: 'GitLab'
    )
  end

  let(:primary_identifier) do
    identifiers.create!(
      project_id: project.id,
      external_type: 'cve',
      name: 'CVE-2021-1234',
      external_id: 'CVE-2021-1234',
      fingerprint: '4c0fe491999f94701ee437588554ef56322ae276'
    )
  end

  let(:finding) do
    findings.create!(
      raw_metadata: raw_metadata,
      project_id: project.id,
      scanner_id: scanner.id,
      primary_identifier_id: primary_identifier.id,
      uuid: '4deb090a-bedf-5ccc-aa9a-ac8055a1ea81',
      project_fingerprint: '1caa750a6dad769a18ad6f40b413b3b6ab1c8d77',
      location_fingerprint: '6d1f35f53b065238abfcadc01336ce65d112a2bd',
      name: 'name',
      report_type: 7,
      severity: 0,
      confidence: 0,
      detection_method: 'gitlab_security_report',
      metadata_version: 'cluster_image_scanning:1.0',
      created_at: "2021-12-10 14:27:42 -0600",
      updated_at: "2021-12-10 14:27:42 -0600"
    )
  end

  subject(:perform) { described_class.new.perform(finding.id, finding.id) }

  context 'with stringified hash as raw_metadata' do
    let(:raw_metadata) do
      '{:location=>{"image"=>"index.docker.io/library/nginx:latest", "kubernetes_resource"=>{"namespace"=>"production", "kind"=>"deployment", "name"=>"nginx", "container_name"=>"nginx", "agent_id"=>"2"}, "dependency"=>{"package"=>{"name"=>"libc"}, "version"=>"v1.2.3"}}}'
    end

    it 'converts stringified hash to JSON' do
      expect { perform }.not_to raise_error

      result = finding.reload.raw_metadata
      metadata = Oj.load(result)
      expect(metadata).to eq(
        {
          'location' => {
            'image' => 'index.docker.io/library/nginx:latest',
            'kubernetes_resource' => {
              'namespace' => 'production',
              'kind' => 'deployment',
              'name' => 'nginx',
              'container_name' => 'nginx',
              'agent_id' => '2'
            },
            'dependency' => {
              'package' => { 'name' => 'libc' },
              'version' => 'v1.2.3'
            }
          }
        }
      )
    end
  end

  context 'with valid raw_metadata' do
    where(:raw_metadata) do
      [
        '{}',
        '{"location":null}',
        '{"location":{"image":"index.docker.io/library/nginx:latest","kubernetes_resource":{"namespace":"production","kind":"deployment","name":"nginx","container_name":"nginx","agent_id":"2"},"dependency":{"package":{"name":"libc"},"version":"v1.2.3"}}}'
      ]
    end

    with_them do
      it 'does not change the raw_metadata' do
        expect { perform }.not_to raise_error

        result = finding.reload.raw_metadata
        expect(result).to eq(raw_metadata)
      end
    end
  end

  context 'when raw_metadata contains forbidden types' do
    using RSpec::Parameterized::TableSyntax

    where(:raw_metadata, :type) do
      'def foo; "bar"; end'     | :def
      '`cat somefile`'          | :xstr
      'exec("cat /etc/passwd")' | :send
    end

    with_them do
      it 'does not change the raw_metadata' do
        expect(Gitlab::AppLogger).to receive(:error).with(message: "expected raw_metadata to be a hash", type: type)

        expect { perform }.not_to raise_error

        result = finding.reload.raw_metadata
        expect(result).to eq(raw_metadata)
      end
    end
  end

  context 'when forbidden types are nested inside a hash' do
    using RSpec::Parameterized::TableSyntax

    where(:raw_metadata, :type) do
      '{:location=>Env.fetch("SOME_VAR")}'           | :send
      '{:location=>{:image=>Env.fetch("SOME_VAR")}}' | :send
      # rubocop:disable Lint/InterpolationCheck
      '{"key"=>"value: #{send}"}'                    | :dstr
      # rubocop:enable Lint/InterpolationCheck
    end

    with_them do
      it 'does not change the raw_metadata' do
        expect(Gitlab::AppLogger).to receive(:error).with(
          message: "error parsing raw_metadata",
          error: "value of a pair was an unexpected type",
          type: type
        )

        expect { perform }.not_to raise_error

        result = finding.reload.raw_metadata
        expect(result).to eq(raw_metadata)
      end
    end
  end

  context 'when key is an unexpected type' do
    let(:raw_metadata) { "{nil=>nil}" }

    it 'logs error' do
      expect(Gitlab::AppLogger).to receive(:error).with(
        message: "error parsing raw_metadata",
        error: "expected key to be either symbol, string, or integer",
        type: :nil
      )

      expect { perform }.not_to raise_error
    end
  end

  context 'when raw_metadata cannot be parsed' do
    let(:raw_metadata) { "{" }

    it 'logs error' do
      expect(Gitlab::AppLogger).to receive(:error).with(message: "error parsing raw_metadata", error: "unexpected token $end")

      expect { perform }.not_to raise_error
    end
  end

  describe '#hash_from_s' do
    subject { described_class.new.hash_from_s(input) }

    context 'with valid input' do
      let(:input) { '{:location=>{"image"=>"index.docker.io/library/nginx:latest", "kubernetes_resource"=>{"namespace"=>"production", "kind"=>"deployment", "name"=>"nginx", "container_name"=>"nginx", "agent_id"=>2}, "dependency"=>{"package"=>{"name"=>"libc"}, "version"=>"v1.2.3"}}}' }

      it 'converts string to a hash' do
        expect(subject).to eq({
          location: {
            'image' => 'index.docker.io/library/nginx:latest',
            'kubernetes_resource' => {
              'namespace' => 'production',
              'kind' => 'deployment',
              'name' => 'nginx',
              'container_name' => 'nginx',
              'agent_id' => 2
            },
            'dependency' => {
              'package' => { 'name' => 'libc' },
              'version' => 'v1.2.3'
            }
          }
        })
      end
    end

    using RSpec::Parameterized::TableSyntax

    where(:input, :expected) do
      '{}'                          | {}
      '{"bool"=>true}'              | { 'bool' => true }
      '{"bool"=>false}'             | { 'bool' => false }
      '{"nil"=>nil}'                | { 'nil' => nil }
      '{"array"=>[1, "foo", nil]}'  | { 'array' => [1, "foo", nil] }
      '{foo: :bar}'                 | { foo: :bar }
      '{foo: {bar: "bin"}}'         | { foo: { bar: "bin" } }
    end

    with_them do
      specify { expect(subject).to eq(expected) }
    end
  end
end
